﻿using System.Collections.Generic;
using System.Linq;
using System.Security;
using MetaCollection.Framework.Model;
using System.Web.Caching;
using System;
using System.Globalization;

namespace MetaCollection.Framework.Security
{
	public static class ItemClaimExtensions
	{
		/// <summary>
		/// Demands the accessRight of the current Thread.Principal given the list of possible permission entries
		/// </summary>
		/// <param name="claims">The entries.</param>
		/// <param name="cp">The CollectionPermissions for the current user</param>
		/// <param name="accessRight">The access right.</param>
		/// <exception cref="System.SecurityException">Security Exception is thrown if the current principal has no roles in the entries with accessRight</exception>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1309:UseOrdinalStringComparison", MessageId = "System.String.Equals(System.String,System.StringComparison)")]
		static void DemandAccess(this IEnumerable<IItemClaim> claims, MetaPrincipal cp, AccessRights accessRight)
		{
			if (cp == null)
				throw new SecurityException("A principal is required for authentication.");

			if (accessRight == AccessRights.None)
				return;

			if (claims != null)
				foreach (IItemClaim claim in claims)
				{
					AccessRights rights = (AccessRights)claim.Rights;
					if ((rights & accessRight) == accessRight)
					{
						var access = from c in cp.Claims
									 where (c.ClaimValue.Equals(claim.ClaimValue, System.StringComparison.InvariantCultureIgnoreCase) && (c.ClaimType.Equals(claim.ClaimType, System.StringComparison.InvariantCultureIgnoreCase)))
									 select c;
						if (access.Any())
							return;
					}
				}
			throw new SecurityException("Access Right Not Granted", typeof(AccessRights), accessRight.ToString());
		}

		/// <summary>
		/// Demands the access right of each item in the collection for the current context's principal.
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="cp">The CollectionPermissions for the current user</param>
		/// <param name="accessRight">The access right.</param>
		/// <exception cref="System.SecurityException">Security exception thrown if no permission record granting the accessright is found for the current principal</exception>
		public static void DemandAccess(this IEnumerable<IItemInstance> items, MetaPrincipal cp, AccessRights accessRight)
		{
			foreach (IItemInstance item in items)
			{
				item.DemandAccess(cp, accessRight);
			}
		}

		/// <summary>
		/// Demands the access.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <param name="cp">The CollectionPermissions for the current user</param>
		/// <param name="accessRight">The access right.</param>
		/// <exception cref="System.SecurityException">Security exception thrown if no permission record granting the accessright is found for the current principal</exception>
		public static void DemandAccess(this IItemInstance item, MetaPrincipal cp, AccessRights accessRight)
		{
			bool? access = item.ItemClaimId == Guid.Empty ? null : GetCacheDecision(item.ItemClaimId, cp, accessRight);

			if (access.HasValue)
			{
				if (!access.Value) // we have a cached value and it was false; deny access.
					throw new SecurityException("Access Right Not Granted", typeof(AccessRights), accessRight.ToString());
				else
					return; // no problems with this demand so return.
			}

			// still here? make our demands.
			item.AccessClaims.DemandAccess(cp, accessRight);
			// still here? they have the requested access rights so store the decision.
			StoreDecision(item.ItemClaimId, cp, accessRight, true);
		}

		/// <summary>
		/// Returns <c>true</c> if the current Thread.Principal has the accessRight given the list of possible permission entries
		/// </summary>
		/// <param name="claims">The entries.</param>
		/// <param name="cp">The CollectionPermissions for the current user</param>
		/// <param name="accessRight">The access right.</param>
		/// <exception cref="System.SecurityException">Security Exception is thrown if the current principal has no roles in the entries with accessRight</exception>
		static bool HasAccess(this IEnumerable<IItemClaim> claims, MetaPrincipal cp, AccessRights accessRight)
		{

			if ((cp == null) || (accessRight == AccessRights.None))
				return false;

			if (claims != null)
				foreach (IItemClaim claim in claims)
				{
					AccessRights rights = (AccessRights)claim.Rights;
					if ((rights & accessRight) == accessRight)
					{
						var access = from c in cp.Claims
									 where ((c.ClaimValue == claim.ClaimValue) && (c.ClaimType == claim.ClaimType))
									 select c;
						if (access.Any())
							return true;
					}
					else
						continue;
				}
			return false;
		}

		/// <summary>
		/// Checks the principals access to the items in the collection
		/// </summary>
		/// <param name="items">The items.</param>
		/// <param name="cp">The CollectionPermissions.</param>
		/// <param name="accessRight">The access right.</param>
		/// <returns>
		/// 	<c>true</c> if the specified items has access; otherwise, <c>false</c>.
		/// </returns>
		public static bool HasAccess(this IEnumerable<IItemInstance> items, MetaPrincipal cp, AccessRights accessRight)
		{
			bool result = false;
			foreach (IItemInstance item in items)
			{
				result = item.HasAccess(cp, accessRight);
				if (!result)
					break;
			}
			return result;
		}

		/// <summary>
		/// Checks the principals access to the items in the collection.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <param name="cp">The CollectionPermissions.</param>
		/// <param name="accessRight">The access right.</param>
		/// <returns>
		/// 	<c>true</c> if the specified item has access; otherwise, <c>false</c>.
		/// </returns>
		/// <exception cref="System.SecurityException">Security exception thrown if no permission record granting the accessright is found for the current principal</exception>
		public static bool HasAccess(this IItemInstance item, MetaPrincipal cp, AccessRights accessRight)
		{
			bool? access = item.ItemClaimId == Guid.Empty ? null : GetCacheDecision(item.ItemClaimId, cp, accessRight);

			if (!access.HasValue)
			{
				access = item.AccessClaims.HasAccess(cp, accessRight);
				StoreDecision(item.ItemClaimId, cp, accessRight, access.Value);
			}

			return access.Value;
		}

		/// <summary>
		/// Gets the security decision from the cache.
		/// Returns null if there is not a decision for this information in the cache.
		/// </summary>
		/// <param name="itemClaimId">The item claim set Id.</param>
		/// <param name="cp">The CollectionPermissions.</param>
		/// <param name="accessRight">The access right.</param>
		/// <returns>true, false or null</returns>
		static bool? GetCacheDecision(Guid itemClaimId, MetaPrincipal cp, AccessRights accessRight)
		{
			return (bool?)Cache[CacheKey(itemClaimId, cp, accessRight)];
		}

		/// <summary>
		/// Stores the has access decision.
		/// </summary>
		/// <param name="securityCode">The security code.</param>
		/// <param name="cp">The CollectionPermissions.</param>
		/// <param name="accessRight">The access right.</param>
		/// <param name="hasAccess">if set to <c>true</c> [has access].</param>
		static void StoreDecision(Guid itemClaimId, MetaPrincipal cp, AccessRights accessRight, bool hasAccess)
		{
			// we can't store a decision without the security hash code.
			if (itemClaimId != Guid.Empty)
				Cache.Insert(CacheKey(itemClaimId, cp, accessRight), hasAccess, null, DateTime.UtcNow.AddMinutes(5), Cache.NoSlidingExpiration);
		}

		/// <summary>
		/// Creates the key used for caching a claims security decision.
		/// </summary>
		/// <param name="itemClaimId">The item claims security id.</param>
		/// <param name="cp">The Claims Principal this decision applies to</param>
		/// <param name="accessRight">The access right.</param>
		/// <returns>A cache key string</returns>
		static string CacheKey(Guid itemClaimId, MetaPrincipal cp, AccessRights accessRight)
		{
			if (cp == null)
				return string.Format(CultureInfo.InvariantCulture, "ClaimDecision={0}|{1}|{2}", itemClaimId, "Anonymous", accessRight);
			else
				return string.Format(CultureInfo.InvariantCulture, "ClaimDecision={0}|{1}|{2}", itemClaimId, cp.Identity.Name, accessRight);
		}

		/// <summary>
		/// A reference to our cache object.
		/// </summary>
		/// <remarks>TODO: When .NET 4.0 comes out with the System.Caching code we will want to rework this.</remarks>
		static Cache Cache
		{
			get
			{
				return (System.Web.HttpContext.Current == null) ? System.Web.HttpRuntime.Cache : System.Web.HttpContext.Current.Cache;
			}
		}
	}
}
